# app/main.py
from fastapi import FastAPI, Depends, HTTPException, status, Request, Form, Body, UploadFile, File
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from typing import List
from sqlalchemy import or_
from datetime import datetime
import math
import json
from pydantic import BaseModel
from pathlib import Path
import uuid
import os
from . import models, schemas, auth, database, crud
from .database import SessionLocal, engine

# 上传限制（允许常见图片与办公文档格式）
ALLOWED_EXT = {'.pdf', '.ppt', '.pptx', '.jpg', '.jpeg', '.png', '.gif', '.doc', '.docx', '.xls', '.xlsx'}
MAX_FILE_SIZE = 20 * 1024 * 1024  # 20MB

models.Base.metadata.create_all(bind=engine)

# Load .env into environment and read secret tokens
try:
    from dotenv import load_dotenv
    load_dotenv()
except Exception:
    # python-dotenv not installed - env vars will still work if supplied by the environment
    pass

COZE_CHAT_TOKEN = os.getenv('COZE_CHAT_TOKEN','')
ADMIN_INVITE_CODE = os.getenv('ADMIN_INVITE_CODE','wzujdgcxy')


# If running against an existing SQLite DB where we added a new column `category`, apply a simple alter table.
inspector = inspect(engine)
try:
    cols = [c['name'] for c in inspector.get_columns('resources')]
    if 'category' not in cols:
        with engine.connect() as conn:
            conn.execute('ALTER TABLE resources ADD COLUMN category VARCHAR')
except Exception:
    # conservative: if the table doesn't exist or DB is not SQLite, ignore and rely on migration tools
    pass

from sqlalchemy import inspect
import logging

logger = logging.getLogger(__name__)

# For safety and clarity we do not modify the database schema automatically here.
# If a legacy DB still contains a `recorder` column in the `meetings` table, we detect it and
# log a clear warning so the end-user can decide the migration approach.
try:
    cols_meetings = [c['name'] for c in inspector.get_columns('meetings')]
    if 'recorder' in cols_meetings:
        logger.warning("Legacy 'recorder' column still exists in 'meetings' table. Please run a DB migration (e.g. Alembic) or follow README instructions to remove it.")
except Exception:
    # ignore if table doesn't exist yet
    pass

app = FastAPI()
templates = Jinja2Templates(directory="app/templates")

    # Helper: make Jinja2 templates simpler by exposing a function for multi-key lookup
def get_field(meeting, content, *keys, default='-'):
    """Return first non-empty field found across meeting attributes and content dict keys.
    Example: get_field(meeting, content, 'meeting_time', '会议时间')
    """
    # check attributes on meeting object first
    try:
        for k in keys:
            if hasattr(meeting, k):
                val = getattr(meeting, k)
                if val:
                    return val
    except Exception:
        pass
    # then check JSON content
    try:
        if isinstance(content, dict):
            for k in keys:
                v = content.get(k)
                if v:
                    return v
    except Exception:
        pass
    return default

def format_participants(out):
    if not out:
        return '-'
    if isinstance(out, str) and ',' in out:
        return out.replace(',', ', ')
    return out

templates.env.globals['get_field'] = get_field
templates.env.globals['format_participants'] = format_participants

# Small convenience: helper to parse attachments JSON consistently
def parse_attachments(raw):
    if not raw:
        return []
    try:
        parsed = json.loads(raw)
    except Exception:
        return []
    out = []
    for a in parsed:
        if isinstance(a, dict):
            out.append(a)
        elif isinstance(a, str):
            out.append({"saved_name": a, "orig_name": a, "remark": ""})
    return out

templates.env.globals['parse_attachments'] = parse_attachments

# Inject token into Jinja globals so templates can access it securely
templates.env.globals['coze_token'] = COZE_CHAT_TOKEN

# 静态文件（用于提供上传的附件）
uploads_dir = Path(__file__).resolve().parent.joinpath('static', 'uploads')
os.makedirs(uploads_dir, exist_ok=True)
app.mount('/uploads', StaticFiles(directory=str(uploads_dir)), name='uploads')
# 挂载整个 static 目录以提供样式、脚本等静态资源（例如 /static/styles.css）
static_dir = Path(__file__).resolve().parent.joinpath('static')
if not static_dir.exists():
    os.makedirs(static_dir, exist_ok=True)
app.mount('/static', StaticFiles(directory=str(static_dir)), name='static')


async def _save_upload_with_limit(up, dest: Path, max_size: int):
    """Save UploadFile to dest path while enforcing max_size. Raises HTTPException on overflow."""
    total = 0
    try:
        with dest.open('wb') as f:
            while True:
                chunk = await up.read(4096)
                if not chunk:
                    break
                total += len(chunk)
                if total > max_size:
                    # remove partial file
                    try:
                        f.close()
                    except Exception:
                        pass
                    if dest.exists():
                        try:
                            dest.unlink()
                        except Exception:
                            pass
                    raise HTTPException(status_code=400, detail=f"文件 {up.filename} 超过大小限制 {max_size} 字节")
                f.write(chunk)
    finally:
        try:
            # reset underlying file pointer if needed
            await up.seek(0)
        except Exception:
            # UploadFile may not support seek in async; ignore
            pass


def _cleanup_saved(saved_list):
    for it in saved_list:
        try:
            if isinstance(it, dict):
                name = it.get('saved_name')
            else:
                name = it
            if not name:
                continue
            p = uploads_dir.joinpath(name)
            if p.exists():
                p.unlink()
        except Exception:
            pass

# Dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

def load_content_json(meeting):
    try:
        return json.loads(meeting.content)
    except Exception:
        return {"raw": meeting.content}

# Session middleware (simple in-memory session for demo)
from starlette.middleware.sessions import SessionMiddleware
app.add_middleware(SessionMiddleware, secret_key="your-secret-key-here-change-in-prod")

def get_current_user(request: Request, db: Session = Depends(get_db)):
    user_id = request.session.get("user_id")
    if not user_id:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    user = db.query(models.User).filter(models.User.id == user_id).first()
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    return user

# --- Routes ---

@app.get("/", response_class=HTMLResponse)
async def home(request: Request, db: Session = Depends(get_db)):
    user_id = request.session.get("user_id")
    if not user_id:
        return RedirectResponse(url="/login")
    user = db.query(models.User).filter(models.User.id == user_id).first()
    # 主页仅为功能入口（按钮），不直接展示会议列表，会议列表放在 /meetings
    message = request.session.pop("message", None)
    error = request.session.pop("error", None)
    return templates.TemplateResponse("home.html", {
        "request": request,
        "user": user,
        "message": message,
        "error": error
    })

@app.get("/meetings", response_class=HTMLResponse)  #查询所有会议记录
async def meetings_list(request: Request, db: Session = Depends(get_db)):
    user_id = request.session.get("user_id")
    if not user_id:
        return RedirectResponse(url="/login")
    user = db.query(models.User).filter(models.User.id == user_id).first()
    # 所有已登录用户均可查看全部记录
    meetings = db.query(models.Meeting).order_by(models.Meeting.created_at.desc()).all()
    # 从 session 中取出一次性消息
    message = request.session.pop("message", None)
    error = request.session.pop("error", None)
    return templates.TemplateResponse("meetings.html", {
        "request": request,
        "user": user,
        "meetings": meetings,
        "message": message,
        "error": error
    })

@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
    # 不传 user，或传 None
    return templates.TemplateResponse("login.html", {"request": request, "user": None})

@app.post("/login")
async def login(
    request: Request,
    username: str = Form(...),
    password: str = Form(...),
    db: Session = Depends(get_db)
):
    # 先检查用户名是否存在
    user_obj = auth.get_user_by_username(db, username)
    if not user_obj:
        # 用户不存在 -> 提示创建账号（使用与首页一致的布局）
        return templates.TemplateResponse("user_not_found.html", {
            "request": request,
            "username": username,
            "user": None
        })

    # 用户存在但密码错误 -> 返回登录页并显示错误
    if not auth.verify_password(password, user_obj.hashed_password):
        request.session["error"] = "密码错误，请检查后重试"
        return RedirectResponse(url="/login", status_code=303)

    # 验证通过，创建会话
    request.session["user_id"] = user_obj.id
    return RedirectResponse(url="/", status_code=303)

@app.get("/register", response_class=HTMLResponse)
async def register_page(request: Request):
    # 支持通过 ?username=xxx 预填用户名（从 user_not_found 页面跳转过来）
    default_username = request.query_params.get("username")
    return templates.TemplateResponse("register.html", {"request": request, "user": None, "default_username": default_username})

@app.post("/register")
async def register(
    request: Request,
    username: str = Form(...),
    password: str = Form(...),
    is_admin: bool = Form(False),
    admin_invite_code: str = Form(None),
    db: Session = Depends(get_db)
):
    if db.query(models.User).filter(models.User.username == username).first(): #
        raise HTTPException(status_code=400, detail="Username already exists")
    hashed_pw = auth.get_password_hash(password)
    # only allow creating admin when invite code matches
    if is_admin:
        if not admin_invite_code or admin_invite_code.strip() != ADMIN_INVITE_CODE:
            request.session['error'] = '管理员邀请码不正确，无法创建管理员账号'
            return RedirectResponse(url='/register', status_code=303)
    new_user = models.User(username=username, hashed_password=hashed_pw, is_admin=is_admin)
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    request.session["user_id"] = new_user.id
    return RedirectResponse(url="/", status_code=303)

@app.get("/logout")
async def logout(request: Request):
    request.session.clear()
    return RedirectResponse(url="/login")

@app.get("/meetings/new", response_class=HTMLResponse)
async def new_meeting_page(request: Request, user=Depends(get_current_user)):
    # 可在此定义模板字段，例如：主题、日期、参会人、要点、待办等
    template_fields = ["主题", "日期", "参会人员", "讨论要点", "待办事项"]
    return templates.TemplateResponse("create_meeting.html", {
        "request": request,
        "user": user,
        "fields": template_fields
    })

@app.post("/meetings")
async def create_meeting(
    request: Request,
    title: str = Form(...),
    # 表单中可以提交单独字段，也可以只提交 content（JSON 字符串）以兼容旧模板
    meeting_time: str = Form(None),
    location: str = Form(None),
    participants: str = Form(None),
    minutes: str = Form(None),
    todos: str = Form(None),
    content: str = Form(None),
    user=Depends(get_current_user),
    db: Session = Depends(get_db),
    attachments: List[UploadFile] = File(None),
    attachment_remarks: List[str] = Form(None)
):
    # 处理上传的附件（如果有），保存到 uploads 目录并收集文件对象（包含原名与备注）
    saved_files = []  # list of dicts {saved_name, orig_name, remark}
    if attachments:
        for idx, up in enumerate(attachments):
            if up.filename:
                suffix = Path(up.filename).suffix.lower()
                if suffix not in ALLOWED_EXT:
                    # cleanup any previously saved files in this request
                    _cleanup_saved(saved_files)
                    raise HTTPException(status_code=400, detail=f"不支持的文件类型: {up.filename}")
                unique_name = f"{uuid.uuid4().hex}{suffix}"
                dest = uploads_dir.joinpath(unique_name)
                # save with size limit
                try:
                    await _save_upload_with_limit(up, dest, MAX_FILE_SIZE)
                except HTTPException:
                    _cleanup_saved(saved_files)
                    raise
                remark = ''
                if attachment_remarks and idx < len(attachment_remarks):
                    remark = attachment_remarks[idx]
                saved_files.append({
                    'saved_name': unique_name,
                    'orig_name': up.filename,
                    'remark': remark or ''
                })

    # 限制总附件数为 6
    if saved_files and len(saved_files) > 6:
        raise HTTPException(status_code=400, detail="最多只能上传 6 个附件")

    # 委托到 CRUD 层（包含附件元数据）
    meeting = crud.create_meeting(
        db=db,
        title=title,
        meeting_time=meeting_time,
        location=location,
        participants=participants,
        minutes=minutes,
        todos=todos,
        content=content,
        created_by=user.id,
        attachments=saved_files
    )
    request.session["message"] = "新建记录成功"
    return RedirectResponse(url="/", status_code=303)


@app.get("/meetings/{meeting_id}/edit", response_class=HTMLResponse)
async def edit_meeting_page(meeting_id: int, request: Request, user=Depends(get_current_user), db: Session = Depends(get_db)):
    meeting = db.query(models.Meeting).filter(models.Meeting.id == meeting_id).first()
    if not meeting:
        raise HTTPException(status_code=404, detail="Meeting not found")
    # 权限判断：只有管理员或创建者可以编辑
    if not (user.is_admin or meeting.created_by == user.id):
        raise HTTPException(status_code=403, detail="没有权限编辑该记录")

    content_data = load_content_json(meeting)

    # 解析附件列表
    attachments = []
    if meeting.attachments:
        try:
            attachments = json.loads(meeting.attachments)
        except:
            attachments = []

    return templates.TemplateResponse("create_meeting.html", {
        "request": request,
        "user": user,
        "meeting": meeting,
        "content": content_data,
        "attachments": attachments,
        "editing": True,
    })


@app.post("/meetings/{meeting_id}/edit")
async def edit_meeting(
    meeting_id: int,
    request: Request,
    title: str = Form(...),
    meeting_time: str = Form(None),
    location: str = Form(None),
    participants: str = Form(None),
    minutes: str = Form(None),
    todos: str = Form(None),
    content: str = Form(None),
    user=Depends(get_current_user),
    db: Session = Depends(get_db),
    attachments: List[UploadFile] = File(None),
    attachment_remarks: List[str] = Form(None),
    delete_attachments: List[str] = Form(None)
):
    meeting = db.query(models.Meeting).filter(models.Meeting.id == meeting_id).first()
    if not meeting:
        raise HTTPException(status_code=404, detail="Meeting not found")
    if not (user.is_admin or meeting.created_by == user.id):
        raise HTTPException(status_code=403, detail="没有权限编辑该记录")

    # 保存新上传文件并追加到现有附件列表
    # 解析现有附件为统一格式（list of dicts）
    existing = []
    if meeting.attachments:
        try:
            parsed = json.loads(meeting.attachments)
            # parsed could be list of strings (old data) or list of dicts
            for it in parsed:
                if isinstance(it, str):
                    existing.append({'saved_name': it, 'orig_name': it, 'remark': ''})
                elif isinstance(it, dict):
                    existing.append({
                        'saved_name': it.get('saved_name') or it.get('name') or '',
                        'orig_name': it.get('orig_name') or it.get('orig') or it.get('name') or '',
                        'remark': it.get('remark', '')
                    })
        except:
            existing = []

    # 处理需要删除的旧附件（在保存新文件之前或之后都可以）
    # delete_attachments contains list of saved_name strings to remove
    if delete_attachments:
        # ensure it's a list
        if isinstance(delete_attachments, str):
            delete_attachments = [delete_attachments]
        new_existing = []
        for item in existing:
            if item.get('saved_name') in delete_attachments:
                # 删除物理文件（若存在）
                try:
                    p = uploads_dir.joinpath(item.get('saved_name'))
                    if p.exists():
                        p.unlink()
                except Exception:
                    pass
            else:
                new_existing.append(item)
        existing = new_existing

    # 保存新上传文件并构造附件对象
    saved_files = []
    if attachments:
        for idx, up in enumerate(attachments):
            if up.filename:
                suffix = Path(up.filename).suffix.lower()
                if suffix not in ALLOWED_EXT:
                    _cleanup_saved(saved_files)
                    raise HTTPException(status_code=400, detail=f"不支持的文件类型: {up.filename}")
                unique_name = f"{uuid.uuid4().hex}{suffix}"
                dest = uploads_dir.joinpath(unique_name)
                try:
                    await _save_upload_with_limit(up, dest, MAX_FILE_SIZE)
                except HTTPException:
                    _cleanup_saved(saved_files)
                    raise
                remark = ''
                if attachment_remarks and idx < len(attachment_remarks):
                    remark = attachment_remarks[idx]
                saved_files.append({
                    'saved_name': unique_name,
                    'orig_name': up.filename,
                    'remark': remark or ''
                })

    # 限制总数不超过 6
    if len(existing) + len(saved_files) > 6:
        raise HTTPException(status_code=400, detail="最多只能有 6 个附件")

    final_attachments = existing + saved_files

    # 更新字段
    meeting.title = title
    meeting.meeting_time = meeting_time
    meeting.location = location
    meeting.participants = participants
    # recorder removed: this code intentionally left out
    meeting.minutes = minutes
    meeting.todos = todos
    meeting.content = content or meeting.content
    meeting.attachments = json.dumps(final_attachments, ensure_ascii=False) if final_attachments else None

    db.add(meeting)
    db.commit()
    request.session["message"] = "更新成功"
    return RedirectResponse(url=f"/meetings/{meeting_id}", status_code=303)

@app.get("/meetings/{meeting_id}", response_class=HTMLResponse)
async def view_meeting(
    meeting_id: int,
    request: Request,
    user=Depends(get_current_user),
    db: Session = Depends(get_db)
):
    meeting = db.query(models.Meeting).filter(models.Meeting.id == meeting_id).first()
    if not meeting:
        raise HTTPException(status_code=404, detail="Meeting not found")
    # 假设 content 是 JSON 字符串
    content_data = load_content_json(meeting)
    # 处理附件列表
    attachments = []
    if meeting.attachments:
        try:
            attachments = json.loads(meeting.attachments)
        except:
            attachments = []
    # 弹性消息
    message = request.session.pop("message", None)
    error = request.session.pop("error", None)

    return templates.TemplateResponse("meeting_detail.html", {
        "request": request,
        "meeting": meeting,
        "content": content_data,
        "user": user,
        "attachments": attachments,
        "message": message,
        "error": error
    })


@app.post("/meetings/{meeting_id}/delete")
async def delete_meeting(
    meeting_id: int,
    request: Request,
    user=Depends(get_current_user),
    db: Session = Depends(get_db)
):
    meeting = db.query(models.Meeting).filter(models.Meeting.id == meeting_id).first()
    if not meeting:
        raise HTTPException(status_code=404, detail="Meeting not found")

    # 权限判断：管理员或创建者可删除
    if not (user.is_admin or meeting.created_by == user.id):
        raise HTTPException(status_code=403, detail="没有权限删除该记录")

    db.delete(meeting)
    db.commit()
    request.session["message"] = "删除成功"
    return RedirectResponse(url="/", status_code=303)

    # 在app/main.py中添加以下代码（建议放在现有路由下方）与 coze工作流上进行http请求查看的接口
@app.get("/api/meetings", response_model=List[schemas.MeetingOut])
async def get_meetings_api(db: Session = Depends(get_db)):
    """无需鉴权的会议记录API接口，供外部服务调用"""
    meetings = db.query(models.Meeting).order_by(models.Meeting.created_at.desc()).all()
    return meetings

# 新增公开创建会议的接口 主要功能是通过coze进行内容的填充。
@app.post("/api/meetings", response_model=schemas.MeetingOut)
async def create_meeting_api(
    meeting: schemas.MeetingCreate,
    db: Session = Depends(get_db)
):
    """无需鉴权的会议记录创建API，供外部服务调用"""
    # 请将1替换为实际存在的用户ID
    default_user_id = 1  #后续需要进行修改
    # 验证必要字段
    if not meeting.title:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="标题不能为空"
        )
    # 解析 content（如果提供）到结构化数据
    content_data = {}
    if meeting.content:
        content_data = load_content_json(meeting)
    # 将单独字段合并到 content_data 中（以便保留完整 JSON）
    if meeting.meeting_time:
        content_data.setdefault("meeting_time", meeting.meeting_time)
        content_data.setdefault("会议时间", meeting.meeting_time)
    if meeting.location:
        content_data.setdefault("location", meeting.location)
        content_data.setdefault("会议地点", meeting.location)
    if meeting.participants:
        content_data.setdefault("participants", meeting.participants)
        content_data.setdefault("参会人员", meeting.participants)
    if meeting.minutes:
        content_data.setdefault("minutes", meeting.minutes)
        content_data.setdefault("会议纪要", meeting.minutes)
    if meeting.todos:
        content_data.setdefault("todos", meeting.todos)
        content_data.setdefault("待办事项", meeting.todos)

    # 委托到 CRUD 层以保持路由简洁
    db_meeting = crud.create_meeting_from_schema(db=db, meeting=meeting, created_by=default_user_id)
    return db_meeting


# 接口可以查新学习资料汇总中的内容。 http请求
@app.get("/api/resources", response_model=List[schemas.ResourceOut])
def read_all_resources(
    db: Session = Depends(get_db),
    # 如需权限控制可添加认证依赖，例如：
    # current_user: schemas.UserOut = Depends(dependencies.get_current_user)
):
    """获取所有学习知识点资料汇总"""
    resources = crud.get_resources(db)  # 调用已有的CRUD方法
    return resources


# -------------------- 学习资料（Resources）模块 --------------------
@app.get("/resources", response_class=HTMLResponse)
async def resources_page(request: Request, db: Session = Depends(get_db), user=Depends(get_current_user)):
    # 支持搜索与筛选：q（关键词）、source、material_type、author_id、日期区间
    q = request.query_params.get('q')
    source = request.query_params.get('source')
    material_type = request.query_params.get('material_type')
    category = request.query_params.get('category')
    author_id = request.query_params.get('author_id')
    created_after = request.query_params.get('created_after')
    created_before = request.query_params.get('created_before')

    # 分页参数
    try:
        page = max(1, int(request.query_params.get('page', 1)))
    except Exception:
        page = 1
    try:
        per_page = int(request.query_params.get('per_page', 10))
    except Exception:
        per_page = 10

    query = db.query(models.Resource)
    if q:
        like = f"%{q}%"
        query = query.filter(or_(models.Resource.core_point.ilike(like), models.Resource.note.ilike(like)))
    if source:
        query = query.filter(models.Resource.source == source)
    if material_type:
        query = query.filter(models.Resource.material_type == material_type)
    if category:
        query = query.filter(models.Resource.category == category)
    if author_id:
        try:
            aid = int(author_id)
            query = query.filter(models.Resource.created_by == aid)
        except Exception:
            pass
    # 日期区间过滤（期望 YYYY-MM-DD）
    if created_after:
        try:
            dt = datetime.strptime(created_after, '%Y-%m-%d')
            query = query.filter(models.Resource.created_at >= dt)
        except Exception:
            pass
    if created_before:
        try:
            dt = datetime.strptime(created_before, '%Y-%m-%d')
            query = query.filter(models.Resource.created_at <= dt)
        except Exception:
            pass

    total = query.count()
    total_pages = max(1, math.ceil(total / per_page))
    if page > total_pages:
        page = total_pages

    resources = query.order_by(models.Resource.created_at.desc()).offset((page - 1) * per_page).limit(per_page).all()

    # 解析附件 JSON 为 Python 列表以便模板显示
    for r in resources:
        attachments = []
        if getattr(r, 'attachments', None):
            try:
                attachments = json.loads(r.attachments)
            except Exception:
                attachments = []
        setattr(r, 'attachments_list', attachments)

    # 作者列表用于筛选下拉
    authors = db.query(models.User).order_by(models.User.username).all()

    return templates.TemplateResponse("resources.html", {
        "request": request,
        "user": user,
        "resources": resources,
        "q": q,
        "source": source,
        "material_type": material_type,
        "category": category,
        "author_id": author_id,
        "authors": authors,
        "created_after": created_after,
        "created_before": created_before,
        "page": page,
        "per_page": per_page,
        "total": total,
        "total_pages": total_pages,
    })


@app.get("/resources/new", response_class=HTMLResponse)
async def new_resource_page(request: Request, user=Depends(get_current_user)):
    return templates.TemplateResponse("resource_form.html", {"request": request, "user": user, "editing": False})


@app.post("/resources")
async def create_resource(
    request: Request,
    core_point: str = Form(...),
    source: str = Form(None),
    material_type: str = Form(None),
    category: str = Form(None),
    note: str = Form(None),
    video_link: str = Form(None),
    user=Depends(get_current_user),
    db: Session = Depends(get_db),
    attachments: List[UploadFile] = File(None),
    attachment_remarks: List[str] = Form(None),
):
    # 处理上传文件并保存到 uploads 目录
    saved_files = []
    if attachments:
        for idx, up in enumerate(attachments):
            if up.filename:
                suffix = Path(up.filename).suffix.lower()
                if suffix not in ALLOWED_EXT:
                    _cleanup_saved(saved_files)
                    raise HTTPException(status_code=400, detail=f"不支持的文件类型: {up.filename}")
                unique_name = f"{uuid.uuid4().hex}{suffix}"
                dest = uploads_dir.joinpath(unique_name)
                try:
                    await _save_upload_with_limit(up, dest, MAX_FILE_SIZE)
                except HTTPException:
                    _cleanup_saved(saved_files)
                    raise
                remark = ''
                if attachment_remarks and idx < len(attachment_remarks):
                    remark = attachment_remarks[idx]
                saved_files.append({"saved_name": unique_name, "orig_name": up.filename, "remark": remark or ''})

    if saved_files and len(saved_files) > 6:
        # cleanup
        _cleanup_saved([f.get('saved_name') for f in saved_files])
        raise HTTPException(status_code=400, detail="最多只能上传 6 个附件")

    res = crud.create_resource(db=db, core_point=core_point, source=source, material_type=material_type, category=category, note=note, created_by=user.id)
    # 保存附件和视频链接到模型
    if saved_files:
        res.attachments = json.dumps(saved_files, ensure_ascii=False)
    if video_link:
        res.video_link = video_link
    if category:
        # update category explicitely
        res.category = category
    db.add(res)
    db.commit()
    request.session["message"] = "已添加学习资料"
    return RedirectResponse(url="/resources", status_code=303)


@app.get("/resources/{resource_id}/edit", response_class=HTMLResponse)
async def edit_resource_page(resource_id: int, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user)):
    res = crud.get_resource(db, resource_id)
    if not res:
        raise HTTPException(status_code=404, detail="资源不存在")
    # 创建者或管理员可编辑
    if res.created_by != user.id and not user.is_admin:
        raise HTTPException(status_code=403, detail="没有权限编辑此条目")
    # 解析附件
    attachments = []
    if res.attachments:
        try:
            attachments = json.loads(res.attachments)
        except:
            attachments = []
    return templates.TemplateResponse("resource_form.html", {"request": request, "user": user, "editing": True, "resource": res, "attachments": attachments})


@app.post("/resources/{resource_id}/edit")
async def edit_resource(
    resource_id: int,
    request: Request,
    core_point: str = Form(...),
    source: str = Form(None),
    material_type: str = Form(None),
    category: str = Form(None),
    note: str = Form(None),
    video_link: str = Form(None),
    db: Session = Depends(get_db),
    user=Depends(get_current_user),
    attachments: List[UploadFile] = File(None),
    attachment_remarks: List[str] = Form(None),
    delete_attachments: List[str] = Form(None),
):
    res = crud.get_resource(db, resource_id)
    if not res:
        raise HTTPException(status_code=404, detail="资源不存在")
    if res.created_by != user.id and not user.is_admin:
        raise HTTPException(status_code=403, detail="没有权限编辑此条目")

    # 解析现有附件
    existing = []
    if res.attachments:
        try:
            parsed = json.loads(res.attachments)
            for it in parsed:
                if isinstance(it, dict):
                    existing.append(it)
                elif isinstance(it, str):
                    existing.append({"saved_name": it, "orig_name": it, "remark": ""})
        except:
            existing = []

    # 处理删除
    if delete_attachments:
        if isinstance(delete_attachments, str):
            delete_attachments = [delete_attachments]
        new_existing = []
        for item in existing:
            if item.get('saved_name') in delete_attachments:
                try:
                    p = uploads_dir.joinpath(item.get('saved_name'))
                    if p.exists():
                        p.unlink()
                except Exception:
                    pass
            else:
                new_existing.append(item)
        existing = new_existing

    # 保存新上传文件
    saved_files = []
    if attachments:
        for idx, up in enumerate(attachments):
            if up.filename:
                suffix = Path(up.filename).suffix.lower()
                if suffix not in ALLOWED_EXT:
                    _cleanup_saved(saved_files)
                    raise HTTPException(status_code=400, detail=f"不支持的文件类型: {up.filename}")
                unique_name = f"{uuid.uuid4().hex}{suffix}"
                dest = uploads_dir.joinpath(unique_name)
                try:
                    await _save_upload_with_limit(up, dest, MAX_FILE_SIZE)
                except HTTPException:
                    _cleanup_saved(saved_files)
                    raise
                remark = ''
                if attachment_remarks and idx < len(attachment_remarks):
                    remark = attachment_remarks[idx]
                saved_files.append({"saved_name": unique_name, "orig_name": up.filename, "remark": remark or ''})

    if len(existing) + len(saved_files) > 6:
        _cleanup_saved([f.get('saved_name') for f in saved_files])
        raise HTTPException(status_code=400, detail="最多只能有 6 个附件")

    final_attachments = existing + saved_files

    # 更新字段
    res.core_point = core_point
    res.source = source
    res.material_type = material_type
    res.category = category
    res.note = note
    res.attachments = json.dumps(final_attachments, ensure_ascii=False) if final_attachments else None
    res.video_link = video_link

    db.add(res)
    db.commit()
    request.session["message"] = "更新成功"
    return RedirectResponse(url="/resources", status_code=303)


@app.post("/resources/{resource_id}/delete")
async def delete_resource(resource_id: int, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user)):
    res = crud.get_resource(db, resource_id)
    if not res:
        raise HTTPException(status_code=404, detail="资源不存在")
    # 仅创建者或管理员可以删除
    if not (user.is_admin or res.created_by == user.id):
        raise HTTPException(status_code=403, detail="没有权限删除此条目")
    crud.delete_resource(db, res)
    request.session["message"] = "已删除"
    return RedirectResponse(url="/resources", status_code=303)

# --- Routes --- coze上的chat 请求
@app.get("/chat", response_class=HTMLResponse)
async def chat_page(request: Request, user=Depends(get_current_user)):
    """聊天页面，需要登录才能访问"""
    return templates.TemplateResponse("chat.html", {
        "request": request,
        "user": user
    })